package org.campuscontent.ui;

import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.swing.event.UndoableEditEvent;

import org.campuscontent.model.AutomataCellViewFactory;
import org.campuscontent.model.AutomataConstants;
import org.campuscontent.model.AutomataConstants.NodeType;
import org.campuscontent.ui.EdgeMaskView.AutomataEdgeEditor;
import org.jgraph.JGraph;
import org.jgraph.graph.CellView;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultPort;
import org.jgraph.graph.EdgeView;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;
import org.jgraph.graph.GraphModel;
import org.jgraph.graph.GraphUndoManager;

public class AutomataGraph extends JGraph implements Serializable
{
    // Undo Manager
    protected GraphUndoManager undoManager;

    // Change Flag
    protected boolean isChanged = false;

    public AutomataGraph( GraphModel model,
                          GraphLayoutCache graphLayoutCache )
    {

        super( model, graphLayoutCache );

        // Control-drag should clone selection
        setCloneable( true );

        // Enable edit without final RETURN keystroke
        setInvokesStopCellEditing( true );

        // When over a cell, jump to its default port (we only have one, anyway)
        setJumpToDefaultPort( true );

        // Specify the Marquee Handler
        
        AutomataMarqueeHandler automataMarqueeHandler = new AutomataMarqueeHandler( this );
        setMarqueeHandler( automataMarqueeHandler );
        addGraphSelectionListener( automataMarqueeHandler );
        addKeyListener( automataMarqueeHandler );

        // Enable the creation of edges
        setPortsVisible( true );

        setAntiAliased( true );

        setGridVisible( true );

        setGridSize( 10 );

        setBackground( new Color( 198, 236, 255 ) );

        setMarqueeColor( Color.red );

        // Create a GraphUndoManager which also Updates the ToolBar
        undoManager = new GraphUndoManager()
        {
            public void undoableEditHappened( UndoableEditEvent e )
            {
                super.undoableEditHappened( e );
                // Then Update The Change Flag
                isChanged = true;
            }
        };

        this.getModel().addUndoableEditListener( undoManager );
    }

    private void writeObject( ObjectOutputStream objectoutputstream ) throws IOException
    {
        Vector vector = new Vector();
        objectoutputstream.defaultWriteObject();
        if ( graphModel instanceof Serializable )
        {
            vector.addElement( "graphModel" );
            vector.addElement( graphModel );
        }
        vector.addElement( "graphLayoutCache" );
        vector.addElement( graphLayoutCache );
        if ( selectionModel instanceof Serializable )
        {
            vector.addElement( "selectionModel" );
            vector.addElement( selectionModel );
        }
        if ( marquee instanceof Serializable )
        {
            vector.addElement( "marquee" );
            vector.addElement( marquee );
        }
        objectoutputstream.writeObject( vector );
        if ( getUIClassID().equals( "GraphUI" ) && ui != null ) ui.installUI( this );
    }

    // create the vertex in the round shape
    protected DefaultGraphCell createDefaultGraphCell()
    {
        if ( idx == -1 )
        {
            idx = countOfNodes(); 
        }
        
        String id = "" + (++idx);
        String name = "Cell " + id;
        
        while ( existNodeNameAlready( name ) )
        {
            name = "Cell " + (++idx);
        }
        
        DefaultGraphCell cell = new DefaultGraphCell( name );

        // by default normal node
        AutomataCellViewFactory.setNodeID( cell, id );
        AutomataCellViewFactory.setNodeName( cell, name );
        AutomataCellViewFactory.setNodeType( cell, AutomataConstants.NodeType.NORMAL );

        cell.addPort();

        return cell;
    }
    
    public int countOfNodes()
    {
        int result = 0;
        CellView[] views = getGraphLayoutCache().getCellViews();

        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();
            // Component renderer = views[ i ].getRendererComponent( this,
            // false, false, false );

            if ( cell instanceof DefaultGraphCell )
            {
                result++;
            }
        }

        return result;
    }
    
    protected DefaultGraphCell createDefaultGraphCell( String nodeLabel, int nodeType )
    {
    	String id = "" + (++idx);
    	
    	DefaultGraphCell cell = new DefaultGraphCell( nodeLabel);

        // by default normal node
        AutomataCellViewFactory.setNodeID( cell, id );
        AutomataCellViewFactory.setNodeName( cell, nodeLabel );
        
        switch ( nodeType )
        {
        	case 0:
        		AutomataCellViewFactory.setNodeType( cell, AutomataConstants.NodeType.NORMAL );
        		break;
        	case 1:
        	 	AutomataCellViewFactory.setNodeType( cell, AutomataConstants.NodeType.START );
        	 	break;
        	case 2:
        		AutomataCellViewFactory.setNodeType( cell, AutomataConstants.NodeType.END );
        		break;
        	case 3:
        		AutomataCellViewFactory.setNodeType( cell, AutomataConstants.NodeType.STARTEND );
        		break;
        }

        cell.addPort();

        return cell;
    }

    public void setNormalNode( DefaultGraphCell node )
    {
        CellView[] views = getGraphLayoutCache().getCellViews();

        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();
            // Component renderer = views[ i ].getRendererComponent( this,
            // false, false, false );

            if ( cell instanceof DefaultGraphCell )
            {
                DefaultGraphCell g = (DefaultGraphCell) cell;

                if ( !AutomataCellViewFactory.hasName( g ) ) continue; // it's
                // not
                // automata
                // node

                if ( g == node )
                {
                    AutomataCellViewFactory.setNodeType( g, NodeType.NORMAL );
                    AutomataCellViewFactory.updateViewType( views[ i ], NodeType.NORMAL );
                }

                Object[] list = { views[ i ].getCell() };
                getGraphLayoutCache().toFront( list );
                getGraphLayoutCache().refresh( views[ i ], false );
            }
        }

        startEditingAtCell( node );
        stopEditing();

        invalidate();
        validate();
        repaint();

        setModel( getModel() );

        clearOffscreen();
    }

    public void setStartNode( DefaultGraphCell node )
    {
        CellView[] views = getGraphLayoutCache().getCellViews();

        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();
            // Component renderer = views[ i ].getRendererComponent( this,
            // false, false, false );

            if ( cell instanceof DefaultGraphCell )
            {
                DefaultGraphCell g = (DefaultGraphCell) cell;

                if ( !AutomataCellViewFactory.hasName( g ) ) continue; // it's
                // not
                // automata
                // node

                if ( g == node )
                {
                    AutomataCellViewFactory.setNodeType( g, NodeType.START );
                    AutomataCellViewFactory.updateViewType( views[ i ], NodeType.START );
                }
                else
                {
                    NodeType type = AutomataCellViewFactory.getNodeType( g );

                    if ( type.equals( NodeType.START ) )
                    {
                        AutomataCellViewFactory.setNodeType( g, NodeType.NORMAL );
                        AutomataCellViewFactory.updateViewType( views[ i ], NodeType.NORMAL );
                    }
                    else if ( type.equals( NodeType.STARTEND ) )
                    {
                        AutomataCellViewFactory.setNodeType( g, NodeType.END );
                        AutomataCellViewFactory.updateViewType( views[ i ], NodeType.END );
                    }
                }

                Object[] list = { views[ i ].getCell() };
                getGraphLayoutCache().toFront( list );
                getGraphLayoutCache().refresh( views[ i ], false );
            }
        }

        startEditingAtCell( node );
        stopEditing();

        invalidate();
        validate();
        repaint();

        // setModel( getModel() );

        clearOffscreen();
    }

    public void setStartEndNode( DefaultGraphCell node )
    {
        CellView[] views = getGraphLayoutCache().getCellViews();

        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();
            // Component renderer = views[ i ].getRendererComponent( this,
            // false, false, false );

            if ( cell instanceof DefaultGraphCell )
            {
                DefaultGraphCell g = (DefaultGraphCell) cell;

                if ( !AutomataCellViewFactory.hasName( g ) ) continue; // it's
                // not
                // automata
                // node

                if ( g == node )
                {
                    AutomataCellViewFactory.setNodeType( g, NodeType.STARTEND );
                    AutomataCellViewFactory.updateViewType( views[ i ], NodeType.STARTEND );
                }
                else
                {
                    NodeType type = AutomataCellViewFactory.getNodeType( g );

                    if ( type.equals( NodeType.START ) )
                    {
                        AutomataCellViewFactory.setNodeType( g, NodeType.NORMAL );
                        AutomataCellViewFactory.updateViewType( views[ i ], NodeType.NORMAL );
                    }
                    else if ( type.equals( NodeType.STARTEND ) )
                    {
                        AutomataCellViewFactory.setNodeType( g, NodeType.END );
                        AutomataCellViewFactory.updateViewType( views[ i ], NodeType.END );
                    }
                }

                Object[] list = { views[ i ].getCell() };
                getGraphLayoutCache().toFront( list );
                getGraphLayoutCache().refresh( views[ i ], false );
            }
        }

        startEditingAtCell( node );
        stopEditing();

        invalidate();
        validate();
        repaint();

        setModel( getModel() );

        clearOffscreen();
    }

    public void setEndNode( DefaultGraphCell node )
    {
        CellView[] views = getGraphLayoutCache().getCellViews();
        
        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();
            
            if ( cell instanceof DefaultGraphCell )
            {
                DefaultGraphCell g = (DefaultGraphCell) cell;
                
                if ( !AutomataCellViewFactory.hasName( g ) ) continue; // it's
                // not
                // automata
                // node
                
                if ( g == node )
                {
                    AutomataCellViewFactory.setNodeType( g, NodeType.END );
                    AutomataCellViewFactory.updateViewType( views[ i ], NodeType.END );
                }
                
                Object[] list = { views[ i ].getCell() };
                getGraphLayoutCache().toFront( list );
                getGraphLayoutCache().refresh( views[ i ], false );
            }
        }
        
        startEditingAtCell( node );
        stopEditing();
        
        invalidate();
        validate();
        repaint();
        
        setModel( getModel() );
        
        clearOffscreen();
    }
    
    public void invertEdge( DefaultEdge edge )
    {
        DefaultGraphCell sc = (DefaultGraphCell)((DefaultPort)edge.getSource()).getParent();
        DefaultGraphCell tg = (DefaultGraphCell)((DefaultPort)edge.getTarget()).getParent();
        
        DefaultEdge invertedEdge = createDefaultEdge();
        Map attr = createEdgeAttributes();
        invertedEdge.getAttributes().applyMap( attr );
        
        String scName = AutomataCellViewFactory.getName( sc );
        String tgName = AutomataCellViewFactory.getName( tg );
        invertedEdge.setUserObject( AutomataEdgeEditor.getLinkName( tgName, scName ) );
        
        GraphConstants.setPoints( invertedEdge.getAttributes(), GraphConstants.getPoints( edge.getAttributes() ) );
        
        
        CellView mappingEdge = graphLayoutCache.getMapping( invertedEdge, !true ); // EdgeView
        graphLayoutCache.update( mappingEdge );
        
        graphLayoutCache.insertEdge( invertedEdge, (DefaultPort)edge.getTarget(), (DefaultPort)edge.getSource() );
        graphLayoutCache.remove( new Object[]{ edge } );
    }
    
    public void invertEdge_OK( DefaultEdge edge )
    {
        DefaultGraphCell sc = (DefaultGraphCell)((DefaultPort)edge.getSource()).getParent();
        DefaultGraphCell tg = (DefaultGraphCell)((DefaultPort)edge.getTarget()).getParent();
        
        DefaultEdge invertedEdge = createDefaultEdge();
        Map attr = createEdgeAttributes();
        invertedEdge.getAttributes().applyMap( attr );
        
        String scName = AutomataCellViewFactory.getName( sc );
        String tgName = AutomataCellViewFactory.getName( tg );
        invertedEdge.setUserObject( AutomataEdgeEditor.getLinkName( tgName, scName ) );
        
        Point2D[] defaultControlPoints = UIUtils.getDefaultControlPoints( tg, 
                                                                          sc,
                                                                          invertedEdge );
        
        ArrayList points = new ArrayList<Point2D>( 3 );
        for ( Point2D pt : defaultControlPoints )
        {
            points.add( pt );
        }
        GraphConstants.setPoints( invertedEdge.getAttributes(), points );
        
        
        CellView mappingEdge = graphLayoutCache.getMapping( invertedEdge, !true ); // EdgeView
        graphLayoutCache.update( mappingEdge );
        
        graphLayoutCache.insertEdge( invertedEdge, (DefaultPort)edge.getTarget(), (DefaultPort)edge.getSource() );
        graphLayoutCache.remove( new Object[]{ edge } );
    }
    
    
    /**
     * Insert a new Vertex at point
     */
    public void insert( Point2D point )
    {
        // Construct Vertex with no Label
        DefaultGraphCell vertex = createDefaultGraphCell();
        // Create a Map that holds the attributes for the Vertex
        vertex.getAttributes().applyMap( createCellAttributes( point ) );
        // Insert the Vertex (including child port and attributes)
        getGraphLayoutCache().insert( vertex );
    }
    
    public void insert( Point2D point , String nodeLabel, int nodeType)
    {
        // Construct Vertex with no Label
        DefaultGraphCell vertex = createDefaultGraphCell(nodeLabel, nodeType);
        // Create a Map that holds the attributes for the Vertex
        vertex.getAttributes().applyMap( createCellAttributes( point ) );
        // Insert the Vertex (including child port and attributes)
        getGraphLayoutCache().insert( vertex );
    }

    /**
     * Hook from GraphEd to set attributes of a new cell
     */
    public Map createCellAttributes( Point2D point )
    {
        Map map = new Hashtable();
        // Add a Bounds Attribute to the Map
        GraphConstants.setBounds( map, new Rectangle2D.Double( point.getX(), point.getY(), 48, 48 ) );
        // Add a nice looking gradient background
        GraphConstants.setGradientColor( map, Color.white );
        // Add a Border Color Attribute to the Map
        GraphConstants.setBorderColor( map, Color.black );
        // Add a White Background
        GraphConstants.setBackground( map, Color.white );
        // Make Vertex Opaque
        GraphConstants.setOpaque( map, true );

        return map;
    }

    // Hook for subclassers
    public DefaultEdge createDefaultEdge()
    {
        DefaultEdge result = new DefaultEdge();
        
        return result;
    }

    // Hook for subclassers
    public Map createEdgeAttributes()
    {
        Map map = new Hashtable();

        // Add a Line End Attribute
        GraphConstants.setLineEnd( map, GraphConstants.ARROW_SIMPLE );

        // Add a Line End Attribute
        GraphConstants.setLineStyle( map, GraphConstants.STYLE_SPLINE );
        
        //GraphConstants.setRouting( map, new SmartRouting() );

        // Add a label along edge attribute
        GraphConstants.setLabelAlongEdge( map, !true );

        // Add control points edition possibility
        GraphConstants.setBendable( map, true );
        
        return map;
    }

    public void setNodeColor( String id, Color cr )
    {
        CellView[] views = getGraphLayoutCache().getCellViews();

        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();
            // Component renderer = views[ i ].getRendererComponent( this,
            // false, false, false );

            if ( cell instanceof DefaultGraphCell )
            {
                DefaultGraphCell g = (DefaultGraphCell) cell;

                if ( AutomataCellViewFactory.hasID( g, id ) ) // node found!
                {
                    GraphConstants.setBorderColor( g.getAttributes(), cr );

                    Object[] list = { views[ i ].getCell() };
                    getGraphLayoutCache().toFront( list );
                    getGraphLayoutCache().refresh( views[ i ], false );

                    return;
                }
            }
        }
    }

    public boolean existNodeNameAlready( String name )
    {
        CellView[] views = getGraphLayoutCache().getCellViews();
        
        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();
            // Component renderer = views[ i ].getRendererComponent( this,
            // false, false, false );
            
            if ( cell instanceof DefaultGraphCell )
            {
                DefaultGraphCell g = (DefaultGraphCell) cell;
                
                String nn = AutomataCellViewFactory.getName( g );

                if ( nn != null && name != null && nn.equals( name ) ) // node found!
                {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    public void setLinkColor( String sourceID, String targetID, Color cr )
    {
        CellView[] views = getGraphLayoutCache().getCellViews(); 

        for ( int i = 0; i < views.length; i++ )
        {
            Object cell = views[ i ].getCell();

            if ( cell instanceof DefaultEdge )
            {
                DefaultEdge g = (DefaultEdge) cell;

                Object objSource = g.getSource();
                Object objTarget = g.getTarget();

                DefaultGraphCell src = (DefaultGraphCell) objSource;
                DefaultGraphCell tar = (DefaultGraphCell) objTarget;

                if ( AutomataCellViewFactory.hasID( (DefaultGraphCell) src.getParent(), sourceID ) && AutomataCellViewFactory.hasID( (DefaultGraphCell) tar.getParent(), targetID ) ) // link
                // found!
                {
                    GraphConstants.setLineColor( g.getAttributes(), cr );

                    Object[] list = { views[ i ].getCell() };
                    getGraphLayoutCache().toFront( list );
                    getGraphLayoutCache().refresh( views[ i ], false );

                    return;
                }
            }
        }
    }

    protected void test()
    {
        // FIXME: explain API to change colors!!
        if ( idx > 10 )
        {
            setNodeColor( "nA", Color.yellow );
            setNodeColor( "nB", Color.blue );
            setNodeColor( "nC", Color.red );
            setNodeColor( "nD", Color.pink );

            setLinkColor( "nA", "nB", Color.green );
            setLinkColor( "nC", "nD", Color.magenta );
        }
    }
    
    public void setToolBar( AutomatToolbar tb )
    {
        toolbar = tb;
    }

    public AutomatToolbar getToolBar()
    {
        return toolbar;
    }
    
    public GraphUndoManager getUndoManager()
    {
        return undoManager;
    }
    
    public static class SmartRouting extends DefaultEdge.DefaultRouting
    {
        @Override
        public List route( GraphLayoutCache cache, EdgeView edge )
        {
            //return super.route( cache, edge );
            
            return null; // give routing to layout control 
        }
    }
    
    public DefaultGraphCell getNodeByName( String nodeName )
    {
    	 GraphModel currentModel = this.getModel();
    	 DefaultGraphCell cell;
    	 
    	 int cellCount = currentModel.getRootCount();
    	 for ( int i = 0; i < cellCount; i++ )
         {

             cell = (DefaultGraphCell) currentModel.getRootAt( i );

             if ( !currentModel.isEdge( cell )&&cell.toString().equals(nodeName)) 
             {
            	 return cell;
             }
             
         }
    	 
    	 return null;
    }

    protected static int idx = -1;
    protected AutomatToolbar toolbar;
}
